package com.m4f.city.utils.serializer;

import java.io.ByteArrayInputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.m4f.city.utils.serializer.annotation.M4FSerializable;
import com.m4f.city.utils.serializer.exception.M4FSerializeException;

public class M4FXmlSerializer extends M4FAbstractSerializer {

	@Override
	public byte[] serialize(Object o) throws M4FSerializeException {
		StringBuffer sb = new StringBuffer("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
		
		if(!isSerializable(o)) throw new M4FSerializeException();
		
		sb.append(serializeObject(o));
		
		return sb.toString().getBytes();
	}
	
	private String serializeObject(Object o) {
		StringBuffer sb = new StringBuffer();
		// Open root element
		sb.append("<").append(getClassAnnotation(o).name()).append(">");
		
		ArrayList<Field> fields = getSerializableFields(o);
		for(Field field : fields) {
			if(!field.isAccessible()) field.setAccessible(true);
			
			// Open property element
			sb.append("<").append(getFieldAnnotation(field).name()).append(">");
			
			// Determining the type of the field. Collection, object...
			if(field.getType().isArray()) {
				sb.append(serializeArray(field, o));
			} else if(isCollection(field)) { // Interface collection
				sb.append(serializeCollection(field, o));
			} else if(isWrapped(field)){
				sb.append(getFieldValueToString(field, o));
			} else if(field.getType().isAnnotationPresent(M4FSerializable.class)) {
				try {
					sb.append(serializeObject(field.get(o)));
				} catch (IllegalArgumentException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			
			// Close property element
			sb.append("</").append(getFieldAnnotation(field).name()).append(">");
		}
		// Close root element
		sb.append("</").append(getClassAnnotation(o).name()).append(">");
		
		return sb.toString();
	}
	
	public <T> T deserialize(Class<T> clazz, byte[] bytes) { // TODO improve this method!!!
		T t = null;
		try {
			t = clazz.newInstance();
			// Get the fields
			 Map<String, Field> fieldsMap = getSerializableFieldsMap(t);
			 
			// Starting the parser
			DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
		    DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
		    Document doc = dBuilder.parse(new ByteArrayInputStream(bytes));
		    doc.getDocumentElement().normalize();
		    doc.getDocumentElement().getNodeName(); // Get the root node name. Not needed
		    Element root = doc.getDocumentElement();
		    // Starting to iterate over the childs
		    for(String fieldName : fieldsMap.keySet()) {
		    	Field field = fieldsMap.get(fieldName);
		    	NodeList nodeList = root.getElementsByTagName(fieldName);
		    	
		    	// Do deserialization
		    	Object deserializedObject = deserializeObject(field, nodeList);
		    
		    	// Set the deserialized value...
		    	field.set(t, deserializedObject);
		    }
		} catch(Exception e) {
			e.printStackTrace();
		}
		
		return t;
	}
	
	
	private Object deserializeObject(Field field, NodeList nodeList) {
		Object fieldValue = null;
		Node node = null;
		node = nodeList.item(0);// TESTING!
		
		if(!field.isAccessible()) field.setAccessible(true);
    	if(field.getType().isArray()) {
    		fieldValue =  deserializeArray(field, node);
    	} else if(isWrapped(field)){
    		if(nodeList.getLength() > 0) {
    			node = nodeList.item(0);
    			fieldValue =  deserializePrimitive(field, node);
    		}
    	} else if(isCollection(field)) {
    		fieldValue = deserializeCollection(field, node);
		} else { // Check if it is an object annotated
    		if(field.getType().isAnnotationPresent(M4FSerializable.class)) { // Its a serializable object...
    			String name = ((M4FSerializable)field.getDeclaringClass().getAnnotation(M4FSerializable.class)).name();
    			Node objectNode = nodeList.item(0);
    			if(objectNode.getNodeName().equals(name)) {
    				fieldValue = deserializeObject(field, objectNode.getChildNodes());
    			}
    		}
    	}
    	
		return fieldValue;
	}
	
	private Object deserializePrimitive(Field field, Node node) {
		return getFieldValueFromString(field, node.getFirstChild().getNodeValue());
	}
	
	private Object deserializeArray(Field field, Node node) {
		NodeList nodeList = node.getChildNodes();
		Object array = Array.newInstance(field.getType().getComponentType(), nodeList.getLength()); // TODO the length is wrong
		
		for(int j = 0 ; j < nodeList.getLength() ; j++) {
			Node arrayElement = nodeList.item(j);
			if (isWrappedArray(field) && arrayElement.getFirstChild().getNodeType() == arrayElement.TEXT_NODE){
				Array.set(array, j, deserializePrimitive(field, arrayElement));
            } else if (arrayElement.getFirstChild().getNodeType() == arrayElement.ELEMENT_NODE) {
            	// TODO
        	}
		}
		
		return array;
	}
	
	private Object deserializeCollection(Field field, Node node) {
		NodeList nodeList = null;
		Collection collection = null;
		
		if(field.getType().equals(List.class)) {
			collection = new ArrayList();
		}
		if(collection != null && node != null) {
			nodeList = node.getChildNodes();
			// Iterate over nodes. If they're items then are primitive else they're objects
			for(int i = 0 ; i < nodeList.getLength() ; i++) {
				Node arrayElement = nodeList.item(i);
				ParameterizedType listType = (ParameterizedType) field.getGenericType();
		        Class<?> listClass = (Class<?>) listType.getActualTypeArguments()[0];
		        
		        if(listClass.isAnnotationPresent(M4FSerializable.class)) {
		        	collection.add(deserializeObject(field, arrayElement.getChildNodes()));
		        } else {
		        	collection.add(getFieldValueFromString(listClass, arrayElement.getFirstChild().getNodeValue()));
		        }
				/*if(arrayElement.getFirstChild().getNodeType() == arrayElement.TEXT_NODE) {
			        collection.add(getFieldValueFromString(listClass, arrayElement.getFirstChild().getNodeValue()));
				} else if (arrayElement.getFirstChild().getNodeType() == arrayElement.ELEMENT_NODE) {
			        if(listClass.isAnnotationPresent(M4FSerializable.class)) {
			        	collection.add(deserializeObject(field, arrayElement.getChildNodes()));
			        }
	        	}*/
			}
		}
			
		return collection;
	}
	
	
	private String serializeArray(Field f, Object o) {
		StringBuffer sb = new StringBuffer();
		Object array;
		
		if(!f.isAccessible()) f.setAccessible(true);
		try {
			array = f.get(o);
			for(int i = 0 ; i < Array.getLength(array) ; i++) {
				sb.append("<item>").append(String.valueOf(Array.get(array, i))).append("</item>");
			}
		} catch (IllegalArgumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return sb.toString();
	}
	
	private String serializeCollection(Field f, Object o) {
		StringBuffer sb = new StringBuffer();
		Collection collection;
		
		if(!f.isAccessible()) f.setAccessible(true);
		try {
			collection = (Collection) f.get(o);
			for(Object obj : collection) {
				if(obj.getClass().isAnnotationPresent(M4FSerializable.class)) {
					sb.append(new String(serializeObject(obj)));
				} else {
					sb.append("<item>").append(String.valueOf(obj)).append("</item>");
				}
			}
		} catch (IllegalArgumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return sb.toString();
	}
	
	private boolean isCollection(Field f) {
		for(Class clazz : f.getType().getInterfaces()) {
			if(clazz.equals(Collection.class)) return true;
		}
		return false;
	}
}
